/*
 *  Copyright (c) 1998, Regents of the University of California
 *  All rights reserved.
 *  Redistribution and use in source and binary forms, with or without
 *  modification, are permitted provided that the following conditions are met:
 *
 *      * Redistributions of source code must retain the above copyright
 *        notice, this list of conditions and the following disclaimer.
 *      * Redistributions in binary form must reproduce the above copyright
 *        notice, this list of conditions and the following disclaimer in the
 *        documentation and/or other materials provided with the distribution.
 *      * Neither the name of the University of California, Berkeley nor the
 *        names of its contributors may be used to endorse or promote products
 *        derived from this software without specific prior written permission.
 *
 *  THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND ANY
 *  EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 *  WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 *  DISCLAIMED. IN NO EVENT SHALL THE REGENTS AND CONTRIBUTORS BE LIABLE FOR ANY
 *  DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
 *  (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 *  LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 *  ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 *  SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package org.demis.wizard;

import java.util.logging.Level;
import java.util.logging.Logger;
import org.demis.wizard.configuration.TemplateConfiguration;
import org.demis.wizard.configuration.DatabaseConfiguration;
import org.demis.wizard.configuration.WizardConfiguration;
import org.demis.wizard.configuration.CollisionTemplateConfiguration;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringWriter;
import java.util.List;
import java.util.Properties;
import java.util.StringTokenizer;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;
import org.apache.velocity.exception.MethodInvocationException;
import org.apache.velocity.exception.ParseErrorException;
import org.apache.velocity.exception.ResourceNotFoundException;
import org.demis.elf.Schema;
import org.demis.elf.Table;
import org.demis.elf.configuration.SupportedDBMS;
import org.demis.elf.dbreader.SchemaReader;
import org.demis.elf.dbreader.generic.GenericSchemaReader;
import org.demis.elf.dbreader.h2.H2SchemaReader;
import org.demis.elf.dbreader.postgresql.PostgresqlSchemaReader;
import org.demis.elf.util.DdlUtils;
import org.demis.elf.util.H2Utils;
import org.demis.orc.DataBaseToObjectConverter;
import org.demis.orc.Mapping;
import org.demis.orc.NameHelper;
import org.demis.troll.ClassDescriptor;
import org.demis.troll.Descriptor;
import org.demis.troll.ObjectModelHelper;
import org.demis.troll.PackageDescriptor;
import org.demis.troll.PropertyDescriptor;
import org.demis.wizard.configuration.FilterConfiguration;
import org.demis.wizard.configuration.filter.TemplateFilter;

/**
 * @version 1.0
 * @author <a href="mailto:demis27@demis27.net">Stéphane kermabon</a>
 */
public class WizardGenerator {

    private static final Log logger = LogFactory.getLog(WizardGenerator.class);
    private WizardConfiguration configuration;
    private NameHelper nameHelper = new NameHelper();

    public WizardGenerator(WizardConfiguration configuration) {
        this.configuration = configuration;
    }
    private Schema schema;
    private List<ClassDescriptor> classes;

    public void generate() {
        schema = readSchema(configuration.getProjectPath(), configuration.getDatabaseConfiguration());
        classes = generateObjectModel(configuration, schema);

        VelocityContext context = new VelocityContext();
        context.put("mapping", Mapping.getInstance());
        context.put("nameHelper", nameHelper);
        context.put("objectModelHelper", ObjectModelHelper.getInstance());
        context.put("databaseConfiguration", configuration.getDatabaseConfiguration());
        context.put("objectConfiguration", configuration.getObjectConfiguration());
        context.put("defaultPackageName", configuration.getObjectConfiguration().getPackageName());
        context.put("configuration", configuration);

        for (TemplateConfiguration templateConfiguration : configuration.getFilesConfiguration()) {
            Template velocityTemplate = initVelocity(templateConfiguration.getTemplateName(), configuration.getTemplatesPath());

            if (velocityTemplate == null) {
                logger.error("template : " + templateConfiguration.getTemplateName() + " not found");
                break;
            }

            context.put("templateConfiguration", templateConfiguration);

            if (templateConfiguration.getTarget().equals("index")) {
                // TODO
            } else if (templateConfiguration.getTarget().equals("column")) {
                for (ClassDescriptor classDescriptor : classes) {
                    for (PropertyDescriptor property: classDescriptor.getProperties()) {
                        if (!filtered(templateConfiguration, property)) {
                            context.put("propertyDescriptor", property);
                            generateFile(velocityTemplate, context);
                        }
                    }
                }
            } else if (templateConfiguration.getTarget().equals("table")) {
                for (ClassDescriptor classDescriptor : classes) {
                    if (!filtered(templateConfiguration, classDescriptor)) {
                        context.put("classDescriptor", classDescriptor);
                        generateFile(velocityTemplate, context);
                    }
                }
            } else if (templateConfiguration.getTarget().equals("schema")) {
//                if (!filtered(templateConfiguration, classDescriptor)) {
                    context.put("schema", schema);
                    generateFile(velocityTemplate, context);
//                }
            }

        }

    }

    private void generateFile(Template velocityTemplate, VelocityContext context) {
        String path = null;
        String filename = null;
        String projectPath = null;
        String checkFilename, collisionFilename;
        FileOutputStream fileOutput = null;
        try {
            TemplateConfiguration templateConfiguration = ((TemplateConfiguration) (context.get("templateConfiguration")));
            ClassDescriptor classDescriptor = (ClassDescriptor) context.get("classDescriptor");
            CollisionTemplateConfiguration collision = ((TemplateConfiguration) (context.get("templateConfiguration"))).getCollision();

            // project path
            projectPath = ((WizardConfiguration) (context.get("configuration"))).getProjectPath();

            // generationFilename
            path = templateConfiguration.getPath();
            filename = projectPath + File.separator + path + File.separator + templateConfiguration.getFileNameTemplate();
            filename = parseFileName(classDescriptor, filename);

            context.put("collision", Boolean.FALSE);
            // check collision
            if (collision != null && collision.isManage()) {

                // checkFilename
                checkFilename = projectPath + File.separator + collision.getCheckPath() + File.separator + templateConfiguration.getFileNameTemplate();
                if (classDescriptor != null) {
                    checkFilename = parseFileName(classDescriptor, checkFilename);
                }
                File checkFile = new File(checkFilename);
                if (checkFile.exists()) {
                    collisionFilename = projectPath + File.separator + collision.getResultPath() + File.separator + collision.getFilename();
                    filename = parseFileName(classDescriptor, collisionFilename);
                    context.put("collision", Boolean.TRUE);
                    logger.info("generate file with filename for collision : " + filename);
                }
            }


            File generationFile = new File(filename);
/*            logger.info("collision for template " + templateConfiguration.getName()
                    + " and " + classDescriptor.getClassName() + " : "
                    + (collision != null ? collision.isManage() : "null")
                    + " file exist : " + filename + " " + generationFile.exists());*/

            File filePath = new File(filename.substring(0, filename.lastIndexOf('/')));
            filePath.mkdirs();

            fileOutput = new FileOutputStream(filename);

            // call template
            StringWriter writer = new StringWriter();
            velocityTemplate.merge(context, writer);
            fileOutput.write(writer.toString().getBytes());

            if (logger.isInfoEnabled()) {
                logger.info("generate file with filename : " + filename);
                logger.info("generate file with path : " + path);
            }

        } catch (ResourceNotFoundException ex) {
            logger.error("Error when generate file : filename = " + filename, ex);
        } catch (ParseErrorException ex) {
            logger.error("Error when generate file : filename = " + filename, ex);
        } catch (MethodInvocationException ex) {
            logger.error("Error when generate file : filename = " + filename, ex);
        } catch (IOException ex) {
            logger.error("Error when generate file : filename = " + filename, ex);
        }
        finally {
            if (fileOutput != null) {
                try {
                    fileOutput.close();
                } catch (IOException ex) {
                    logger.error("Error when generate file : filename = " + filename, ex);
                }
            }
        }
    }

    public Schema readSchema(String projectPath, DatabaseConfiguration databaseConfiguration) {

        org.demis.elf.configuration.DatabaseConfiguration elfConfiguration = null;
        if (databaseConfiguration.getUrl() != null) {
            elfConfiguration = new org.demis.elf.configuration.DatabaseConfiguration();
            elfConfiguration.setUrl(databaseConfiguration.getUrl());
            elfConfiguration.setDbms(SupportedDBMS.POSTGRESQL8);
            elfConfiguration.setPassword(databaseConfiguration.getPassword());
            elfConfiguration.setSchemaName(databaseConfiguration.getSchema());
            elfConfiguration.setUser(databaseConfiguration.getUser());
            elfConfiguration.setFilter(databaseConfiguration.getFilter());
            elfConfiguration.setReadData(databaseConfiguration.isReadData());
        } else if (databaseConfiguration.getH2scripts() != null) {
            elfConfiguration = H2Utils.createDatabase(projectPath, databaseConfiguration.getH2scripts()) ;
        }
        else if (databaseConfiguration.getDdl() != null) {
            elfConfiguration = DdlUtils.createDatabase(databaseConfiguration.getDdl()) ;
        }

        SchemaReader schemaReader;
        if (elfConfiguration.getDbms() == SupportedDBMS.POSTGRESQL8) {
            logger.info("read postgresql schema");
            schemaReader = new PostgresqlSchemaReader();
            return schemaReader.read(elfConfiguration);
        } else if (elfConfiguration.getDbms() == SupportedDBMS.H2) {
            logger.info("read generic schema");
            schemaReader = new H2SchemaReader();
            return schemaReader.read(elfConfiguration);
        } else if (elfConfiguration.getDbms() == SupportedDBMS.GENERIC) {
            logger.info("read generic schema");
            schemaReader = new GenericSchemaReader();
            return schemaReader.read(elfConfiguration);
        }
        else {
            logger.info("Not supported DBMS : " + databaseConfiguration.getType());
        }

        return null;
    }

    public List<ClassDescriptor> generateObjectModel(WizardConfiguration configuration, Schema schema) {
        List<ClassDescriptor> classes = DataBaseToObjectConverter.convertSchema(schema);


        for (ClassDescriptor classDescriptor : classes) {
            Table table = Mapping.getInstance().getTable(classDescriptor);
            PackageDescriptor packageDescriptor = new PackageDescriptor();
            packageDescriptor.setName(configuration.getObjectConfiguration().getPackageName() + "." + nameHelper.getPackageName(classDescriptor));

            classDescriptor.setPackageDescriptor(packageDescriptor);
            Mapping.getInstance().addMapping(table, classDescriptor);
        }

        return classes;
    }

    protected Template initVelocity(String templateName, String templatePath) {
        if (logger.isInfoEnabled()) {
            logger.info("templateName " + templateName);
            logger.info("templatePath " + templatePath);
        }
        VelocityEngine ve = new VelocityEngine();
        try {
            Properties p = new Properties();
            //p.setProperty("file.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");
            if (templatePath != null) {
                p.setProperty("file.resource.loader.path", templatePath);
            }
            ve.init(p);
            return ve.getTemplate(templateName);
        } catch (ResourceNotFoundException ex) {
            logger.error("template #" + templatePath + "/" + templateName + " not found", ex);
        } catch (ParseErrorException ex) {
            logger.error("template #" + templatePath + "/" + templateName + " not found", ex);
        } catch (Exception ex) {
            logger.error("template #" + templatePath + "/" + templateName + " not found", ex);
        }
        return null;
    }

    private String parseFileName(ClassDescriptor classDescriptor, String fileNameTemplate) {
        String result = fileNameTemplate;
        if (classDescriptor != null) {
            result = result.replaceAll("\\{className\\}", classDescriptor.getClassName());
//            result = result.replaceAll("\\{objectName\\}", classDescriptor.getObjectName());
            PackageDescriptor packageDescriptor = classDescriptor.getPackageDescriptor();
            if (packageDescriptor != null) {
                result = result.replaceAll("\\{packageName\\}", convertPackageNameToPath(packageDescriptor.getName()));
            }
        }
        result = result.replaceAll("\\{defaultpackageName\\}", convertPackageNameToPath(configuration.getObjectConfiguration().getPackageName()));
        logger.info("parseFileName( " + fileNameTemplate + " ) to " + result);
        return result;
    }

    /**
     * Convert a package name like aaa.bbb.ccc to directories path like aaa/bbb/ccc. We use the system file separator.
     * @param pakageName The name of the package
     * @return The relative path as a String.
     */
    private static String convertPackageNameToPath(String pakageName) {
        StringTokenizer tokenizer = new StringTokenizer(pakageName, ".");
        StringBuffer buffer = new StringBuffer("");
        while (tokenizer.hasMoreTokens()) {
            buffer.append(File.separator);
            buffer.append(tokenizer.nextToken());
        }
        return buffer.toString();
    }

    public List<ClassDescriptor> getClasses() {
        return classes;
    }

    public Schema getSchema() {
        return schema;
    }

    public boolean filtered(TemplateConfiguration templateConfiguration, Descriptor descriptor) {
        // Unit test
        if (templateConfiguration != null 
                && descriptor != null
                && templateConfiguration.getFilters() != null
                && templateConfiguration.getFilters().size() > 0) {
            for (FilterConfiguration filter: templateConfiguration.getFilters()) {
                try {
                    TemplateFilter templateFilter = (TemplateFilter) (Class.forName(filter.getClassname()).newInstance());
                    if (templateFilter.acceptMode(templateConfiguration.getTarget())) {
                        if (templateFilter.filterTemplate(descriptor)) {
                            return true;
                        }
                    }
                } catch (ClassNotFoundException ex) {
                    // TODO Log
                } catch (InstantiationException ex) {
                    // TODO Log
                } catch (IllegalAccessException ex) {
                    // TODO Log
                }
            }
            return false;
        }
        else {
            return false;
        }
    }
}
